#!/usr/bin/env python
# _*_ coding: utf-8


from operator import  itemgetter
import datetime
import collections

def join(list_of_things):
    return ", ".join(map(str,list_of_things))

#adapted from python docs
def safeattrgetter(*items):
    def resolve_attr(obj, attr):
        for name in attr.split("."):
            obj = getattr(obj, name, '')
        return obj

    if len(items) == 1:
        attr = items[0]
        def g(obj):
            return resolve_attr(obj, attr)
    else:
        def g(obj):
            return tuple(resolve_attr(obj, attr) for attr in items)
    return g

class Formatter(object):
    """Formatter([width, divider, indent, char])
    Class for formating objects in columns.
    The Formatter class takes one optional parameter:

    :param width: maximum character width
    :type width: int or None
    :param divider: column divider
    :type divider: single character or None
    :param indent: number of char to indent second lines
    :type indent: int
    :param char: single character used in indenting

    The main public method is `format_objects` which takes objects and a list
    of columns. Columns indicate attributes.

    Attribute formatters are called `stringers` and return a
    (string, fillchar, alignmentchar) tuple. See the Python docs on string
    formatting.

    Attributes are formatted by their type.

    Custom attribute formatters can be added to the formatter via the
    `add_stringer` method.
    """
    #default stringifiers based on type
    _defaults = (
        ((str,unicode), ('{}', '','<')),
        ((bool,), ("{}",'','<')),
        ((int,), ("{:,d}", '', '>')),
        ((float,), ("{:,.2f}", '', '>')),
        ((list, tuple), (join, '','<')),
        ((datetime.date,), ("{:%b %d, %Y}", '', '<'))

        )
    def __init__(self, width=None, divider=None, indent=None, char=None):
        self._stringers = {}
        self._width = (78 if width is None else int(width))
        self._last_cols = None
        self._column_divider = (' ' if divider is None else str(divider)[0])
        self._indent = (4 if indent is None else int(indent))
        self._indent_char = (' ' if char is None else str(char)[0])


    def _stringify(self, something, col=None):
        "return a string, fill character, and alignment code"
##        print "stringifying", something, col
        func = None
        if col and col.lower() in self._stringers:
##            print "using custom stringer", col.lower(), self._stringers[col.lower()]
            func, fill, align = self._stringers[col.lower()]

        if not func:
            for type_search, result in self._defaults:
    ##            print type_search, type(something)
                if isinstance(something, type_search):
                    func, fill, align = result
                    break

        if not func:
            return ('?', '', '^')

        if isinstance(func, collections.Callable):
            stringed = func(something)
        else:
            stringed = func.format(something)

        return (stringed, fill, align)


    def format_objects(self, stuff, cols = None):
        "stuff - list of things. cols = list of column headers"
        res = []

        if cols is None:
            cols = self._last_cols

        if cols is None:
            raise ValueError('Need a list of columns')

        if isinstance(cols, collections.Iterable):
            thinggetter = safeattrgetter(*cols)
            self._last_cols = cols

        elif isinstance(cols, basestring):
            thinggetter = safeattrgetter(cols.split())
            self._last_cols = cols.split()

        else:
            raise ValueError("cannot use given columns")

        # stringify returns a string, fill, and align tuple
        pre_formats = [map(self._stringify, thinggetter(thing), cols)
            for thing in stuff]
##        print "pre_formats", pre_formats

        _headers = [map(str.title, cols)]
        stringed_things = list(_headers)+[map(itemgetter(0),a) for a in pre_formats]

        fills = [map(itemgetter(1), a) for a in pre_formats]
        alignments = [map(itemgetter(2),a) for a in pre_formats]


        # find the length of each item
        lengths = [map(len, thing) for thing in stringed_things]
##        print "lengths", lengths

        # get the maximum lengths of each column
        maxes = [reduce(max, l) for l in zip(*lengths)]
##        print "maxes", maxes

        # build the format string
##        _blocks = ['{{0[{0}]:{{1[{0}]}}{{2[{0}]}}{{3[{0}]}}}}'.format(x) for x in range(len(maxes))]
##        _string = ' '.join(_blocks)
##        header_fills = ['']*len(fills[0])

        # break the lines if needed
##        length = sum(maxes) + len(maxes) - 1
##        print length, length < self._width

        width = 0
        line_width = self._width
        indent = False
        line_headers = [[],] # used for headers
        line_items = [[],] # used for items
        for header, max_, align, fill in zip(_headers[0], maxes, alignments[0], fills[0]):
            new_width = width + max_ + 1

            if new_width < line_width:
                width = new_width
                line_headers[-1].append((header, max_, align))
            else:
                indent = True
                if indent and self._indent:
                    # subtract one so the line begins after _indent
                    ind = self._indent-1
                    new_line=[(self._indent_char*ind, ind, '<')]
                else:
                    new_line = []
                new_line.append((header, max_, align))
##                line_headers.append([(header, max_, align)])
                line_headers.append(new_line)
                width = self._indent
                new_width =  width + max_ + 1


##            print width, new_width

##        for line in line_headers:
##            print "line_header:", line
        # line_headers now lists the lines needed to present the columns

        # here I need to create the _blocks and _string for each line in line_headers
        # each line in the header gets it's own header fills
        longest_line = 0
        headers = []
        one_block = '{{0[{0}]:{{1[{0}]}}{{2[{0}]}}{{3[{0}]}}}}'
        for line in line_headers:
            these_lengths = map(itemgetter(1), line)
            this_line_length = sum(these_lengths) + len(these_lengths) -1
##            print these_lengths, this_line_length
            longest_line = max(longest_line, this_line_length)
            these_blocks = [one_block.format(x) for x in range(len(these_lengths))]
            headers.append(self._column_divider.join(these_blocks))

##        print "$"*longest_line

        indent = False
        for line, header in zip(headers, line_headers):
##            print map(itemgetter(0), header)
##            print line
##            print header
            these_headers = map(itemgetter(0), header)
            these_maxes = map(itemgetter(1), header)
            these_fills = ['']*len(header)
            these_alignments = map(itemgetter(2), header)
            res.append( line.format(these_headers, these_fills, these_alignments, these_maxes))
            indent = True

        res.append('-'*longest_line)

        for thing in stringed_things[1:]:
##            print "stringed_thing", thing
            thing_iter = iter(thing)
            fill_iter = iter(fills[0])
            for line, header in zip(headers, line_headers):
##                print len(line.split()), line, header
                stringed_stuff = []
                these_fills = []
                for head_tuple in header:
                    if head_tuple[0].strip(): # should check for fillchar
                        try:
                            stringed_stuff.append(next(thing_iter))
                            these_fills.append(next(fill_iter))
                        except StopIteration:
                            pass
                    else:
                        stringed_stuff.append('')
                        these_fills.append('')
                these_maxes = map(itemgetter(1), header)
                these_alignments = map(itemgetter(2), header)
##                print stringed_stuff
##                print these_fills
##                print these_alignments
##                print these_maxes
                newline = line.format(stringed_stuff, these_fills, these_alignments, these_maxes)
##                print newline
                res.append(newline)  # check if new line is empty if should be appended

##                SPLIT THESE_MAXES, FILLS, ETC from HEADER (AS ABOVE)
##                FOR EACH ITTEM IN HEADER, IF Item[0] is not emppty, pull next from thing_iter

##        res.append('#'*self._width)
##        res.append(_string.format(_headers[0], header_fills, alignments[0], maxes))
##        res.append('-'*(sum(maxes)+ len(maxes)-1))
##        for t in stringed_things[1:]:
##            res.append(_string.format(t, fills[0], alignments[0], maxes))

    ##        res.append( "{0[0]:{1[0]}} {0[1]:<{1[1]}} {0[2]:<{1[2]}}".format(t,maxes) )

        return res

    def add_stringer(self, name, format_or_callable, fill='', align='<'):
        """add_stringer(name, format_or_callable[, fill, align])
        Add a special format handler for the attribute name.

        :param name: attribute name to handle
        :type name: string
        :param format_or_callable: individual formatter
        :type format_or_callable: string or callable
        :param fill: character to fill the string
        :type fill: single character
        :param align: alignment character
        :type align: either '>', '<', or '^'

        See the Python docs on string formatting for the details.

        If the format_or_callable parameter is callable, it must accept the
        attribute name to be formatted, and return a string.
        """
        self._stringers[name] = (format_or_callable, fill, align)

if __name__=='__main__':
    class Thing(object):
        pass

    thing = Thing()
    thing.name='tet'
    thing.number = False
    thing.date = datetime.date(2013,4,1)
    thing.cost = 1.24
    thing.stuff = (1, 2, 'buckle my shoe')

    other = Thing()
    other.name='other'
##    other.number = 1000
    other.date = datetime.date(2013,9,7)
    other.cost = 1435.13
    other.stuff = (3, 4, 'close the door')
    ##print thing
    ##
    ##print '\n'.join(format_objects([thing],['name', 'number', 'date']))

    F = Formatter(60)
##    F.add_stringer('date', '{:%d %B %Y}', '', '>')

    print "\n".join(F.format_objects([thing, other], "name number cost date stuff".split()) )
